In [1]:
import pandas as pd
import os, re
import numpy as np
import soundfile as sf
from IPython.display import clear_output
import pickle
import librosa
from sklearn.preprocessing import StandardScaler
from tensorflow.keras.utils import to_categorical
import tensorflow as tf
from tensorflow.keras import layers, models
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping
from tensorflow.keras.models import load_model
from tensorflow.keras import Model
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.metrics import det_curve, DetCurveDisplay
import plotly.express as px
import plotly
import matplotlib.pyplot as plt
import plotly.graph_objects as go
In [3]:
long_data_frame_LSTM = pd.read_csv("C:/Users/zbugo/Desktop/praktyki_zadania/20/data/long_data_frameLSTM.csv")
long_data_frame_GRU = pd.read_csv("C:/Users/zbugo/Desktop/praktyki_zadania/20/data/long_data_frameGRU.csv")
long_data_frame_RNN = pd.read_csv("C:/Users/zbugo/Desktop/praktyki_zadania/20/data/long_data_frameRNN.csv")
In [4]:
df_curve_list = []
colors = []
model_name = []
all_data = []
list_for_FRR_FAR = []

LSTM.¶

In [5]:
genuine = long_data_frame_LSTM[long_data_frame_LSTM['genuine']]
impostor = long_data_frame_LSTM[~long_data_frame_LSTM['genuine']]

data = genuine, impostor
all_data.append(data)
In [6]:
FAR, FRR, thresholds = det_curve(y_true=long_data_frame_LSTM['genuine'], y_score=long_data_frame_LSTM['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))

df_curve = pd.DataFrame({
    'False Acceptance Rate': FAR[::10],
    'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"

df_curve_list.append(df_curve)
colors.append('blue')
model_name.append('LSTM')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)

GRU.¶

In [7]:
genuine = long_data_frame_GRU[long_data_frame_GRU['genuine']]
impostor = long_data_frame_GRU[~long_data_frame_GRU['genuine']]

data = genuine, impostor
all_data.append(data)
In [8]:
FAR, FRR, thresholds = det_curve(y_true=long_data_frame_GRU['genuine'], y_score=long_data_frame_GRU['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]

df_curve = pd.DataFrame({
    'False Acceptance Rate': FAR[::10],
    'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"

df_curve_list.append(df_curve)
colors.append('red')
model_name.append('GRU')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)

RNN.¶

In [9]:
genuine = long_data_frame_RNN[long_data_frame_RNN['genuine']]
impostor = long_data_frame_RNN[~long_data_frame_RNN['genuine']]

data = genuine, impostor
all_data.append(data)
In [10]:
FAR, FRR, thresholds = det_curve(y_true=long_data_frame_RNN['genuine'], y_score=long_data_frame_RNN['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]

df_curve = pd.DataFrame({
    'False Acceptance Rate': FAR[::10],
    'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"

df_curve_list.append(df_curve)
colors.append('brown')
model_name.append('Classic RNN')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)

Wykresy.¶

Wszystkie modele zostały wytrenowane na podstawie macierzy MFCC, połączonej z macierzami pierwszej i drugiej pochodnej obliczonymi na podstawie MFCC, aby zapewnić lepsze i dokładniejsze uczenie modelu.¶

In [11]:
# Tworzenie figure z jednym wykresem w kolumnie i tyloma wierszami, ile wynosi długość all_data.
fig, axes = plt.subplots(len(all_data), 1, figsize=(14, 6 * len(all_data)))  

# Iteracja przez dane i rysowanie wykresów.
for i in range(len(all_data)):
    ax = axes[i]  # Wybór odpowiedniego osi (subplotu).

    # Rysowanie histogramów dla genuine i impostor.
    ax.hist(all_data[i][0]['score'], bins=128, alpha=0.5, label='genuine')
    ax.hist(all_data[i][1]['score'], bins=128, alpha=0.5, label='impostor')
    
    # Ustawienia osi i tytułu.
    ax.set_xlim(-1, 1)
    ax.legend()
    ax.set_title(model_name[i])
    ax.grid()

# Wyświetlanie wykresów.
plt.tight_layout()
plt.show()
No description has been provided for this image

Pierwsze dwa histogramy, odpowiadające sieciom neuronowym LSTM i GRU, są do siebie bardzo podobne. Różnice między nimi są zauważalne, ale stosunkowo niewielkie. Natomiast ostatni histogram, przedstawiający klasyczny RNN, znacząco się różni. Wartości score dla genuine i impostor nachodzą na siebie, co sugeruje, że embeddingi wygenerowane przez ten model są bardzo słabe. Dobre odseparowanie przy użyciu dowolnego thresholdu wydaje się niemożliwe. Może postprocessing mógłby częściowo poprawić wyniki tego modelu. Kolejne wykresy dostarczą więcej informacji na temat jakości analizowanych modeli.

In [12]:
plotly.offline.init_notebook_mode()
# Lista kolorów dla różnych modeli

# Tworzenie pustego wykresu
fig_thresh = go.Figure()

# Iteracyjne dodawanie krzywych do wykresu
for i, df_curve in enumerate(df_curve_list):
    legend_group = model_name[i]  # Grupa legendy dla modelu

    # Dodanie linii dla FAR (z przypisaną grupą legendy)
    fig_thresh.add_trace(go.Scatter(
        x=df_curve.index, y=df_curve['False Acceptance Rate'], 
        mode='lines', 
        line=dict(color=colors[i]),
        name='FAR',
        legendgroup=legend_group,  # Przypisanie do grupy
        showlegend=False  # Ukrycie wpisu dla FAR w legendzie
    ))
    
    # Dodanie linii dla FRR (z przypisaną grupą legendy)
    fig_thresh.add_trace(go.Scatter(
        x=df_curve.index, y=df_curve['False Rejection Rate'], 
        mode='lines', 
        line=dict(color=colors[i]),  # Inny styl dla FRR (przerywana linia)
        name='FRR',
        legendgroup=legend_group,  # Przypisanie do grupy
        showlegend=False  # Ukrycie wpisu dla FRR w legendzie
    ))

    # Znalezienie najlepszego threshold (EER)
    arg_of_best_threshold = np.argmin(np.abs(df_curve['False Acceptance Rate'] - df_curve['False Rejection Rate']))
    x = df_curve.index[arg_of_best_threshold]
    y = np.mean([df_curve['False Acceptance Rate'].iloc[arg_of_best_threshold], df_curve['False Rejection Rate'].iloc[arg_of_best_threshold]])

    # Dodanie punktu dla EER (z przypisaną grupą legendy)
    fig_thresh.add_trace(go.Scatter(
        x=[x], y=[y], 
        mode='markers', 
        marker=dict(size=10, color=colors[i]),
        name='EER',
        legendgroup=legend_group,  # Przypisanie do grupy
        showlegend=False  # Ukrycie wpisu dla EER w legendzie
    ))

    # Dodanie tylko jednego wpisu do legendy dla całego modelu
    fig_thresh.add_trace(go.Scatter(
        x=[None], y=[None],  # Wpis do legendy bez dodawania nowych danych
        mode='lines',
        line=dict(color=colors[i]),
        name=model_name[i],  # Nazwa modelu w legendzie
        legendgroup=legend_group,  # Przypisanie do tej samej grupy
        showlegend=True  # Pokaż w legendzie
    ))

# Dostosowanie osi i tytułu
fig_thresh.update_layout(
    title="FRR, FAR EER for types of RNN",
    xaxis_title="Thresholds",
    width=1200,
    height=600
)

# Wyświetlenie wykresu
fig_thresh.show()

Na wykresie można odczytać wartości FAR i FRR dla różnych wartości thresholdu, co umożliwia znalezienie kompromisu w postaci EER, czyli punktu przecięcia się krzywych FAR i FRR. Im mniejsza wartość EER, tym wyższa skuteczność modelu.

Oprócz samej wartości EER warto również przeanalizować dynamikę FAR i FRR, co może być przydatne przy podejmowaniu decyzji, czy model ma działać w sposób "zachłanny", akceptując wysokie wartości FAR (model user-friendly), czy w sposób restrykcyjny, akceptując wysokie wartości FRR (model bezpieczny).

Analizując krzywe FAR i FRR dla różnych modeli, najgorzej wypada klasyczne RNN – jego wartość EER jest najwyższa, co wskazuje na niską skuteczność tego modelu. Z kolei LSTM i GRU wykazują bardzo podobne przebiegi krzywych FAR i FRR, a ich wartości EER są zbliżone. Jeśli jednak konieczne byłoby wskazanie najlepszego modelu na podstawie wartości EER, LSTM wypada minimalnie lepiej.

Przy tym wyborze sugerowałem się wyłącznie wartością EER. Warto jednak pamiętać, że model o najniższym EER nie zawsze będzie najlepszym rozwiązaniem, w zależności od tego, czy priorytetem jest zachłanność (niski FRR, wysoki FAR), czy bezpieczeństwo (niski FAR, wysoki FRR). W takich przypadkach szczególnie ważne staje się dokładne przeanalizowanie dynamiki obu wskaźników.

In [13]:
plotly.offline.init_notebook_mode()

fig = go.Figure()

for i in range(0,len(list_for_FRR_FAR)):
    FAR, FRR = list_for_FRR_FAR[i]
    fig.add_trace(go.Scatter(x=FAR, y=FRR, name=model_name[i], mode='lines'))

fig.update_layout(
    title='Krzywa DET',
    xaxis_title='FAR',
    yaxis_title='FRR',
    width=1200,
    height=600
)

fig.update_xaxes(range=[0, 0.35])
fig.update_yaxes(range=[0, 0.55])


fig.show()

Wykres przedstawia krzywą DET, która ilustruje, jak zmienia się FRR w zależności od FAR. Na wykresie można również odczytać wartość EER – punkt, w którym FAR i FRR są równe. Ten punkt jest jedyny i reprezentuje wartość EER.

Wykres jest szczególnie przydatny w sytuacjach, gdy jeden rodzaj błędu jest bardziej akceptowalny niż drugi. Na przykład w systemach bezpieczeństwa może być lepiej nie dopuścić właściciela konta bankowego do wypłaty pieniędzy niż pozwolić na dostęp złodziejowi. W takim przypadku możemy sprawdzić, jaka jest wartość FRR przy akceptowalnym dla nas poziomie FAR.

Analizując wykres, od razu odrzucamy model klasycznego RNN – krzywa DET jednoznacznie wskazuje, że jest zdecydowanie najgorszy. Przy wyborze najlepszego modelu musimy zastanowić się, czy zależy nam bardziej na modelu "zachłannym" (user-friendly) czy "restrykcyjnym" (bezpiecznym).

  • W przypadku modelu "zachłannego" powinniśmy wybrać GRU, ponieważ przy niskiej wartości FRR (fałszywych odrzuceń) osiąga najlepszą wartość FAR (fałszywych akceptacji).
  • W przypadku modelu "restrykcyjnego" wybór nie jest już tak oczywisty. Krzywe DET dla LSTM i GRU przecinają się i nachodzą na siebie w początkowych wartościach osi x (dla niskiego FAR). Aby podjąć ostateczną decyzję, należy przyjrzeć się wykresowi w innej skali. Może to pozwolić na dokładniejszą analizę i wybór najlepszego modelu w tej kategorii.
In [14]:
fig, ax = plt.subplots(figsize=(18, 8))  # Ustaw szerokość i wysokość w calach

# Tworzenie obiektu DetCurveDisplay i rysowanie wykresu
for i in range(0, len(list_for_FRR_FAR)):

    FAR, FRR = list_for_FRR_FAR[i]
    display = DetCurveDisplay(fpr=FAR, fnr=FRR)
    display.plot(ax=ax, color = colors[i], label = model_name[i])  # Przekazanie osi 'ax' do metody plot()

ax.set_title('Krzywa DET (Detection Error Tradeoff).')

# Wyświetlenie wykresu
plt.grid();
No description has been provided for this image

Przyglądając się wykresowi DET w innej, bardziej czytelnej skali (choć wykres ma wadę, ponieważ nie jest interaktywny), zauważamy, że w skrajnych wartościach – gdy model ma być bardzo bezpieczny (niskie wartości FAR) – również powinniśmy wybrać GRU. Dzieje się tak, ponieważ przy niskiej wartości FAR osiąga on najniższą wartość FRR.

Co ciekawe, jeśli jednak chcielibyśmy wybrać model skrajnie "zachłanny", to w bardzo ekstremalnych przypadkach należałoby rozważyć wybór LSTM lub nawet klasycznego RNN. Klasyczny RNN mógłby być interesujący tylko w takich skrajnych sytuacjach, mimo że ogólnie wypada znacznie gorzej w innych zakresach.

Który model najszybciej się uczy?¶

In [ ]:
model_names = ['LSTM', 'GRU', 'RNN']
time_of_learning = []

path_1 = 'C:/Users/zbugo/Desktop/praktyki_zadania/20/modele_itp/'
path_2 = '_time.npy'


for name in model_names:
    one_time = np.load(path_1 + name + path_2)
    time_of_learning.append(one_time)


for name, time in zip(model_names, time_of_learning):
    print(f"Model {name} uczył się przez {time:.2f} sekund podczas 20 pierwszych epok.")
Model LSTM uczył się przez 975.91 sekund podczas 20 pierwszych epok.
Model GRU uczył się przez 1035.58 sekund podczas 20 pierwszych epok.
Model RNN uczył się przez 664.74 sekund podczas 20 pierwszych epok.

Bez zaskoczenia można zauważyć, że klasyczne RNN uczyło się najszybciej, co sprawia, że pod względem czasu wypada najkorzystniej. Ciekawie robi się jednak, gdy przyjrzymy się wynikom LSTM i GRU. Zakładałem, że to LSTM będzie uczyć się najwolniej, ale tak się nie stało – gorzej wypadło GRU. Nie potrafię jednoznacznie uzasadnić tego wyniku; być może model ten nie był dobrze zoptymalizowany. Na początku stworzyłem dobrze działającą sieć LSTM, a następnie kopiowałem całą architekturę, zmieniając jedynie warstwy rekurencyjne (z LSTM na GRU i klasyczne RNN). Długość nauki badałem na podstawie 20 pierwszych epok, ponieważ każdy model kończył swoje działanie po różnej liczbie epok.